package client;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Semaphore;

import parameters.RLM_Parameters;

import utils.ConnectionBuffer;
import utils.Constants;
import utils.GenerateID;


/**
 * Klasa konekcije izmedju servera i klijenta. Svaka konekcija sadrzi referencu
 * na kreirani socket, IP adrese i portove komunicirajucih tacaka, kao i
 * bafere za pisanje i citanje podataka iz soketa.
 * 
 * @author UrosKrkic & Radivoje Popovic
 */
public class ClientSocketConnection implements Constants {
	
	/**
	 * Semafor koji obezbedjuje da getMessage() metod bude blokiran dok ne pristigne
	 * poruka za slanje, tj. dok se ne pozove setMessage() metod.
	 */
	private Semaphore waitForMessage = new Semaphore(1, true);
	
	
	/**
	 * Semafor obezbedjuje da ce se pri startovanju Reader i Writer thread-a, pre izvrsiti
	 * getMessage() metod, nego setMessage(), jer su pre kreiranja i startovanja thread-ova
	 * oba semafora zakljucana.
	 */
	private Semaphore messageAccepted = new Semaphore(1, true);
	
	
	/**
	 * Buffer-i komandi za citanje podataka iz socketa-a. Koriste se u Reader thread-u
	 * kako bi se znalo da li podatke iz soketa upucivati nekoj komandi (kroz njen buffer)
	 * ili procesirati podatke u Reader thread-u.
	 * Za svaku komandu se kreira po jedan ovakav buffer, i u zavisnosti od koda komande
	 * koja je poslala zahtev serveru, odgovor se upisuje u odgovarajuci buffer indeksiran
	 * kodom komande, odakle se podaci iscitavaju. 
	 */
	public ConnectionBuffer[] buffers = new ConnectionBuffer[COMMAND_COUNT];
	
	
	/**
	 * Buffer za citanje i pisanje podataka u socket. Koristi se u setMessage() i getMessage()
	 * metodi, kao amortizer brzina poziva vise setMessage() metoda i brzine ispisa poruke
	 * u stream iz Writer thread-a kada getMessage() vrati poruku. 
	 */
	private ConnectionBuffer buffer = new ConnectionBuffer(1024);
	
	
	/**
	 * Socket.
	 */
	private Socket socket;
	
	
	/**
	 * IP adresa servera.
	 */
	private InetAddress serverAddress;
	
	
	/**
	 * IP adresa klijenta.
	 */
	private InetAddress clientAddress;
	
	
	/**
	 * Port serverskog procesa.
	 */
	private int serverPort;
	
	
	/**
	 * Port klijentskog procesa.
	 */
	private int clientPort;
	
	
	/**
	 * Stream od servera ka klijentu.
	 */
	private BufferedReader inFromClient;
	
	
	/**
	 * Stream od klijenta ka serveru.
	 */
	private PrintWriter outToClient;
	
	
	/**
	 * Jedinstveni ID konekcije. Automatski se dodeljuje prilikom kreiranja konekcije.
	 */
	private long connectionID = 0;
	
	/**
	 * Username korisnika koji je vlasnik konekcije.
	 */
	private String username;
	
	
	/**
	 * Reader thread.
	 */
	private ClientThreadReader clientThreadReader;
	
	
	/**
	 * Writer thread.
	 */
	private ClientThreadWriter clientThreadWriter;
	
	
	/**
	 * Index buffer-a u koji ce Reader thread da upisuje podatke koje procita iz straem-a.
	 */
	private int bufferIndex;
	
	
	/**
	 * Referenca na objekat aplikacije.
	 */
	private ClientApplication theApp;
	
	
	/**
	 * Oznacava tip konekcije, odnosno da li je konekcija uspostavljena izmedju klijenta i servera, ili je
	 * konekcija uspostavljena izmedju dva klijenta.
	 * Ako je konekcija uspostavljena izmedju klijenta i servera, tip konekcije je CLIENT_SERVER_CONNECTION.
	 * Ako je konekcija uspostavljena izmedju dva klijenta, tip konekcije je P2P_CONNECTION.
	 */
	private int connectionType = -1;
	
	
	
	/**
	 * Konstruktor.<br>
	 * Kreira instancu ServerSocketConnection klase i vrsi inicijalizacije atributa klase
	 * (objekta).<br>
	 * Za svaki kreirani socket podesava se timeout. Ukoliko  se nista ne prenese
	 * u toku timeout perioda, konekcija se prekida.
	 * @param theApp - referenca na objekat aplikacije
	 * @param socket - socket koji je dodeljen konekciji
	 * @param connectionType - tip konekcije: za client-server, tip je CLIENT_SERVER_CONNECTION;
	 * za client-client, tip je P2P_CONNECTION
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public ClientSocketConnection(ClientApplication theApp, Socket socket, int connectionType) {
		this.theApp = theApp;
		this.socket = socket;
		this.connectionType = connectionType;
		
		for (int i = 0; i < buffers.length; i++) {
			buffers[i] = new ConnectionBuffer(50);
		}
		
		this.connectionID = GenerateID.generateID();
		
		initialize();
		
		
		
		createReaderAndWriterThread();
	}
	
	
	/**
	 * Metod vraca jedinstveni ID konekcije.
	 * @return - ID konekcije.
	 */
	public long getConnectionID() {
		return this.connectionID;
	}

	
	/**
	 * Metod setuje ID konekcije.
	 * @param id konekcije
	 */
	public void setConnectionID(long id) {
		this.connectionID = id;
	}
	
	
	/**
	 * Metod setuje logovanog korisnika koji predstavalja vlasnika konekcije.
	 * @param username - vlasnik konekcije
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public void setUsername(String username) {
		this.username = username;
	}
	
	
	/**
	 * Metod vraca username korisnika koji je vlasnik konekcije.
	 * @return - vlasnik konekcije
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public String getUsername() {
		return this.username;
	}
	
	
	/**
	 * Metod kreira Reader i Writer thread. Inicijalno se zakljucavaju oba semafora, kako bi
	 * se obezbedilo da ce se getMessage() metod izvrsiti pre nego sto neko pozove setMessage().
	 * @author UrosKrkic & Radivoje Popovic
	 */
	private void createReaderAndWriterThread() {
		try {
			waitForMessage.acquire();
			messageAccepted.acquire();
		}
		catch (InterruptedException ie) {
			ie.printStackTrace();
		}

		clientThreadReader = new ClientThreadReader(theApp, this);
		clientThreadWriter = new ClientThreadWriter(this);
	}
	
	
	/**
	 * Metoda koja pise podatke u konekciju (socket). Koristi se za slanje podataka
	 * od servera ka klijentu.
	 * @param data - string koji ce biti poslat klijentu
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public void writeData(String data) {
		getWriter().println(data);
	}
	
	
	/**
	 * Metod cita podatke iz ulaznog stream-a sa klijenta.
	 * @return - procitani podaci sa servera
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public String readData() {
		String response = null;
		
		try {
			response = this.inFromClient.readLine();
			if (response == null)
				throw new IOException("inFromClient closed");
		}
		catch (IOException ioe) {
			response = null;
			/*
			 * Ukoliko je konekcija tipa: RLM Client - RLM Server, kada se prepozna prekid ove konekcije,
			 * klijent se mora izlogovati, jer je to znak da je server zavrsio karijeru. :):):)
			 */
			//TODO ovo treba proveriti
			if (connectionType == CLIENT_SERVER_CONNECTION) {
				if ( !socket.isClosed() && theApp.getState() == LOGGED_STATE )
					simulateLogout();
			}
			
//			ioe.printStackTrace();
		}
		
		return response;
	}
	
	
	
	/**
	 * Metoda radi prislilni logout korisnika, kada dodje do prekida konekcije sa RLM serverom.
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public void simulateLogout() {
		
		theApp.setState(INITIAL_STATE);
		
		Set<String> keys = ClientApplication.clientBinding.keySet();
		
		Iterator<String> keysIterator = keys.iterator();
		
		while ( keysIterator.hasNext() ) {
			String key = keysIterator.next();
			
			ClientBindObject cbo = ClientApplication.clientBinding.get(key);
			if (cbo.getDialog() != null) {
				cbo.getDialog().setConnection(null);
				cbo.getDialog().dispose();
			}
			if (cbo.getConnection() != null)
				cbo.getConnection().quitConnection();
			cbo.setConnection(null);
		}
		
		ClientApplication.clientBinding.clear();
		
		theApp.getClientModel().clearModel();
		
		theApp.getWindow().refreshWindow();
		
		theApp.retrieveSocketConnection().quitConnection();
		
		theApp.setSocketConnection(null);
		
		theApp.stopClientServer();
		
		try {
			ClientApplication.clientServerSemaphore.acquire();
		}
		catch (InterruptedException e1) {
//			e1.printStackTrace();
		}
		
		theApp.writeInputParameters();
		RLM_Parameters.storeParameters();
		
		ClientApplication.multicastReceived = false;
	}
	
	
	
	/**
	 * Metod vrsi inicijalizaciju atributa (polja) klase.
	 * @author UrosKrkic & Radivoje Popovic
	 */
	private void initialize() {
		serverAddress = socket.getLocalAddress();
		serverPort = socket.getLocalPort();
		clientAddress = socket.getInetAddress();
		clientPort = socket.getPort();

		try {
			inFromClient = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));			
			outToClient = new PrintWriter( new BufferedWriter( 
					new OutputStreamWriter( socket.getOutputStream(),  "UTF-8" ) ), true );
		}
		catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	
	/**
	 * Metod vraca referencu na ulazni stream od klijenta ka serveru.
	 * @return - ulazni stream od klijenta ka serveru
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public BufferedReader getReader() {
		return this.inFromClient;
	}
	
	
	/**
	 * Metod vraca referencu na izlazni stream od servera ka klijentu.
	 * @return - izlazni stream od servera ka klijentu
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public PrintWriter getWriter() {
		return this.outToClient;
	}
	
	
	/**
	 * Metod vraca IP adresu klijenta.
	 * @return - IP adresa klijenta
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public InetAddress getClientAddress() {
		return clientAddress;
	}

	/**
	 * Metod vraeca port na kome se izvrsava klijentski proces.
	 * @return - broj porta klijenta
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public int getClientPort() {
		return clientPort;
	}

	/**
	 * Metod vraca IP adresu servera.
	 * @return - IP adresa servera
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public InetAddress getServerAddress() {
		return serverAddress;
	}

	/**
	 * Metod vraca broj porta na kome se izvrsava serverski proces.
	 * Ovo nije broj dobro poznatog porta serverskog soketa, vec broj
	 * porta koji je dodeljen nakon accept-a TCP konekcije.
	 * @return - broj porta servera
	 */
	public int getServerPort() {
		return serverPort;
	}
	
	
	/**
	 * Metod zatvara konekciju. Pod zatvaranjem konekcije podrazumeva se
	 * zatvaranje streamova, kao i zatvaranje socket-a izmedju klijenta i
	 * servera.
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public void quitConnection() {
		try {
			this.socket.shutdownInput();
			this.socket.shutdownOutput();
			clientThreadWriter.interrupt();
			this.inFromClient.close();
			this.outToClient.close();
			this.socket.close();
			clientThreadReader.interrupt();
		}
		catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}
	
	
	
	/**
	 * Metod vraca index buffera u koji Reader thread upisuje podatke.
	 * @return
	 */
	public int getBufferIndex() {
		return this.bufferIndex;
	}
	
	
	/**
	 * Metod koji inicira slanje poruke klijentu. Kada se pozove ovaj metod, poruka
	 * prosledjena kao argument upisuje se u buffer, i poziva se release() semafora, kako bi
	 * se obavestio uspavani Writer thread da je poruka spremna za slanja i nakon
	 * toga Writer thread vrsi slanje poruke, tj. pisanje poruke u stream.
	 * @param message - poruka koja se salje
	 * @param index - index buffer-a u koji ce Reader thread da upisuje podatke
	 * koje procita iz stream-a.
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public synchronized void setMessage(int index, String message) {
		this.bufferIndex = index;
		
		buffer.write(message);
		
		waitForMessage.release();
		
		try {
			messageAccepted.acquire();
		}
		catch (InterruptedException ie) {
			ie.printStackTrace();
		}
	}

	
	/**
	 * Ovaj metod se uvek poziva na pocetku svake iteracije Writer threada. Pozivom
	 * ove metode, Writer thead se odmah stavlja u block stanje (semaforom), dok ne
	 * pristigne poruka koju treba poslati, a to se desava kada se pozove metod setMessage().
	 * release() iz metoda setMessage() uzrokuje da Writer thread nastavi izvrsavanje
	 * kroz ovaj metod, sto uzrokuje da ce metod vratiti poruku postavljenu setMessage()
	 * metodom. Nakon toga, Writer thread je dobio poruku, moze da je ispise u stream,
	 * nakon cega se proces ponavlja.
	 * Poruke se uzimaju iz buffer-a koji se puni setMessage() metodom. Na ovaj nacin
	 * je izvrsena amortizacija brzina u pozivu setMessage() metoda i ispisa poruka
	 * u stream.
	 * @return - poruka koja se ispisuje u stream
	 * @author UrosKrkic & Radivoje Popovic
	 */
	public String getMessage() {
		String tempMessage = "";
		try {
			waitForMessage.acquire();
			
			tempMessage = buffer.read();
			
			messageAccepted.release();
		}
		catch (InterruptedException ex) {
//			ex.printStackTrace();
			return null;
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		
		return tempMessage;
	}

}
